package com.shelpe.services.lexicon.controller;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.DeferredResult;

import com.shelpe.services.lexicon.constants.Device;
import com.shelpe.services.lexicon.constants.Mode;
import com.shelpe.services.lexicon.constants.Origin;
import com.shelpe.services.lexicon.constants.Status;
import com.shelpe.services.lexicon.dto.RecommendWordDto;
import com.shelpe.services.lexicon.entity.AdgroupRecord;
import com.shelpe.services.lexicon.entity.KeywordRecommend;
import com.shelpe.services.lexicon.entity.KeywordRelation;
import com.shelpe.services.lexicon.properties.AutoDriveParams;
import com.shelpe.services.lexicon.repository.mongo.AdgroupRecordRepository;
import com.shelpe.services.lexicon.repository.mongo.KeywordRecommendRepository;
import com.shelpe.services.lexicon.repository.mongo.KeywordRelationRepository;
import com.shelpe.services.lexicon.service.KeywordMetricService;
import com.shelpe.services.lexicon.service.KeywordRecommendPriceService;
import com.shelpe.services.lexicon.service.KeywordRecommendService;
import com.shelpe.services.lexicon.service.central.FrequencyCentralWordExtractor;
import com.shelpe.services.lexicon.utils.WordUtils;

import lombok.extern.slf4j.Slf4j;

@Controller
@RequestMapping(path="/lexiconservice/adgroups")
@Slf4j
public class KeywordsController implements InitializingBean{
	private final static Map<String, String> MAPPINGS = new HashMap<String, String>();
	
	private final static Map<String, String> MOBILE_MAPPINGS = new HashMap<String, String>();
	
	@Autowired
	private AdgroupRecordRepository adgroupRecordRepository;
	@Autowired
	private KeywordRecommendRepository keywordRecommendRepository;
	@Autowired
	private KeywordRelationRepository keywordRelationRepository;
	@Autowired
	private KeywordRecommendService keywordRecommendService;
	@Autowired
	private KeywordRecommendPriceService keywordRecommendPriceService;
	@Autowired
	private KeywordMetricService keywordMetricService;
	@Autowired
	private AutoDriveParams params;
	@Autowired
	private FrequencyCentralWordExtractor centralWordExtractor;
	
	@RequestMapping(method = RequestMethod.GET,path="/{adgroup_id}/recommends")
	@ResponseBody
	public List<RecommendWordDto> smartWords(@PathVariable("adgroup_id") long adgroupID,
			@RequestParam(name="mode", defaultValue="0")int modeValue,
			@RequestParam(name="device", defaultValue="0")int deviceValue,
			@RequestParam(name="sort", defaultValue="pv")String sort,
			@RequestParam(name="direction", defaultValue="desc")String directionValue,
			@RequestParam(name="maxPrice", defaultValue="0")float maxPrice,
			@RequestParam(name="defaultPrice", defaultValue="0")float defaultPrice){
		Mode mode = Mode.from(modeValue);
		Direction direction = Direction.fromStringOrNull(directionValue);
		if(direction == null){
			direction = Direction.DESC;
		}
		Device device = Device.from(deviceValue);
		sort = this.covertSort(sort, device);
		
		List<AdgroupRecord> records = this.adgroupRecordRepository.findByAdgroupIDAndRecommendCountGreaterThanOrderByPostTimeDesc(adgroupID, 0);
		AdgroupRecord record = this.filterByStatus(records, Status.calc);
		if(record == null){
			record = this.filterByStatus(records, Status.calcing);
		}
		if(record == null){
			record = this.filterByStatus(records, Status.sync);
		}
		if(record == null){
			record = this.filterByStatus(records, Status.syncing);
		}
		if(record == null){
			throw new IllegalArgumentException("record_not_found");
		}
		
		//prepare words
		List<Origin> origins = new ArrayList<Origin>();
		origins.add(Origin.taobao);
		origins.add(Origin.combine);
		List<KeywordRecommend> recommends = this.keywordRecommendRepository.findByAdgroupRecordIDAndOriginIn(record.getRecordID(), origins, new Sort(new Order(direction, sort)));
		log.debug("{} recommends are found: {}", recommends.size(), record);
		
		if(record.getTagTime() == null){
			this.keywordRecommendService.syncRecommendTags(recommends, record);
			record.setTagTime(new Date());
			record.setLastModifiedTime(new Date());
			this.adgroupRecordRepository.save(record);
		}
		
		//filter words
		recommends = this.keywordRecommendService.filterRecommends(recommends, record, device);
		
		//pricing
		if(record.getPqrTime() != null && !DateUtils.isSameDay(new Date(), record.getPqrTime())){//cahce for one day as performance
			this.keywordRecommendPriceService.fetchPqr(mode.name(), recommends, record, device);
			record.setPqrTime(new Date());
			record.setLastModifiedTime(new Date());
			this.adgroupRecordRepository.save(record);
		}
		this.keywordRecommendPriceService.fetchPrice(mode.name(), recommends, record, device, (int) (defaultPrice * 100), (int) (maxPrice * 100));
		
		List<String> centeralWords = new ArrayList<String>();
		List<KeywordRecommend> centralKrs = this.centralWordExtractor.extract(recommends);
		if(centralKrs != null && centralKrs.size() > 0){
			for(KeywordRecommend kr: centralKrs){
				centeralWords.add(kr.getWord());
			}
		}
		
		List<RecommendWordDto> dtos = new ArrayList<RecommendWordDto>();
		Map<String, KeywordRecommend> convertedWords = new HashMap<String, KeywordRecommend>();
		for(KeywordRecommend kr:recommends){
			KeywordRecommend addedKr = convertedWords.get(kr.getWord());
			
			RecommendWordDto dto = new RecommendWordDto();
			dto.setWord(kr.getWord());
			dto.from(kr, device);
			dto.from(kr.getTags());
			
			if(centeralWords.contains(kr.getWord())){
				dto.setCenteral(true);
			}
			
			if(addedKr != null){
				log.debug("combined word is prior to other word: {}", kr);
				if(addedKr.getOrigin() != Origin.combine && kr.getOrigin() == Origin.combine){
					int index = -1;
					for(RecommendWordDto wd: dtos){
						index = index + 1;
						if(wd.getWord().equals(addedKr.getWord())){
							break;
						}
					}
					dtos.add(index, dto);
					convertedWords.put(kr.getWord(), kr);
				}
			}else{
				dtos.add(dto);
				convertedWords.put(kr.getWord(), kr);
			}
		}
		dtos = this.sort(dtos, sort, direction);
		return dtos;
	}
	
	/**
	 * 除了利用mongo来排序，我们需要对BigDecimal字段进行排序，因为BigDecimal字段在mongo中是字符串。
	 * @param recommends
	 * @param sort
	 * @return
	 */
	private List<RecommendWordDto> sort(List<RecommendWordDto> dtos, final String sort, final Direction direct){
		if(sort.indexOf(".cpc") > -1 || sort.indexOf(".coverage") > -1){
			Collections.sort(dtos, new Comparator<RecommendWordDto>(){
				@Override
				public int compare(RecommendWordDto o1, RecommendWordDto o2) {
					int ret = 0;
					
					if(sort.indexOf(".cpc") > -1){
						ret = (int) ((o2.getAveragePrice() - o1.getAveragePrice()) * 100);
					}else if(sort.indexOf(".coverage") > -1){
						ret = (int) ((o2.getClickCoverage() - o1.getClickCoverage()) * 100);
					}
					return direct == Direction.DESC?ret:-ret;
				}
			});
		}
		
		return dtos;
	}
	
	@RequestMapping(method = RequestMethod.GET,path="/{adgroup_id}/keywords")
	@ResponseBody
	public DeferredResult<List<RecommendWordDto>> keywords(@PathVariable("adgroup_id") long adgroupID,
			@RequestParam(name="words")String[] words,
			@RequestParam(name="mode", defaultValue="0")int modeValue,
			@RequestParam(name="device", defaultValue="0")int deviceValue,
			@RequestParam(name="sort", defaultValue="pv")String sort,
			@RequestParam(name="direction", defaultValue="desc")String directionValue,
			@RequestParam(name="maxPrice", defaultValue="0")final float maxPrice,
			@RequestParam(name="defaultPrice", defaultValue="0")final float defaultPrice){
		final DeferredResult<List<RecommendWordDto>> result = new DeferredResult<List<RecommendWordDto>>();
		
		final Mode mode = Mode.from(modeValue);
		Direction direction = Direction.fromStringOrNull(directionValue);
		if(direction == null){
			direction = Direction.DESC;
		}
		final Device device = Device.from(deviceValue);
		sort = this.covertSort(sort, device);
		
		if(words != null && words.length > 0){
			Date yesterday = DateUtils.addDays(DateUtils.truncate(new Date(), Calendar.DATE), -1);
			Date start = DateUtils.addDays(yesterday, -params.getStateDays());
			
			List<AdgroupRecord> records = this.adgroupRecordRepository.findByAdgroupID(adgroupID, new Sort(new Order(Direction.DESC, "postTime")));
			final AdgroupRecord record = (records != null && records.size() > 0)?records.get(0):null;
			if(record == null){
				throw new IllegalArgumentException("record_not_found");
			}
			
			List<String> wordList = new ArrayList<String> ();
			for(String word: words){
				wordList.add(word);
			}
			
			final List<KeywordRecommend> recommends = this.keywordRecommendRepository.findByAdgroupIDAndWordIn(adgroupID, wordList);
			for(KeywordRecommend recommend: recommends){
				int index = wordList.indexOf(recommend.getWord());
				if(index > -1){
					wordList.remove(index);
				}
			}
			
			if(wordList.size() > 0){
				List<KeywordRecommend> manualRecommends = new ArrayList<KeywordRecommend>();
				for(String word: wordList){
					KeywordRecommend recommend = new KeywordRecommend();
					recommend.setOrigin(Origin.manual);
					recommend.setAdgroupID(record.getAdgroupID());
					recommend.setAdgroupRecordID(record.getRecordID());
					recommend.setWord(word);
					recommend.setWeight(71);
					
					recommend.setCreateTime(new Date());
					recommend.setLastModifiedTime(new Date());
					manualRecommends.add(recommend);
				}
				this.keywordMetricService.syncInsightData4Recommends(manualRecommends, start, yesterday, record, 
						new KeywordMetricService.Callback() {
							
							@Override
							public void call(List<KeywordRecommend> fetcheRecommends) {
								keywordRecommendRepository.save(fetcheRecommends);
								keywordRecommendService.syncRecommendTags(fetcheRecommends, record);
								recommends.addAll(fetcheRecommends);
								
								keywordRecommendPriceService.fetchPqr(mode.name(), recommends, record, device);
								keywordRecommendPriceService.fetchPrice(mode.name(), recommends, record, device, (int) (defaultPrice * 100), (int) (maxPrice * 100));
								List<RecommendWordDto> dtos = new ArrayList<RecommendWordDto>();
								for(KeywordRecommend kr:recommends){
									RecommendWordDto dto = new RecommendWordDto();
									dto.setWord(kr.getWord());
									dto.from(kr, device);
									dto.from(kr.getTags());
									dtos.add(dto);
								}
								result.setResult(dtos);
							}
						});
			}else{
				keywordRecommendPriceService.fetchPqr(mode.name(), recommends, record, device);
				keywordRecommendPriceService.fetchPrice(mode.name(), recommends, record, device, (int) (defaultPrice * 100), (int) (maxPrice * 100));
				List<RecommendWordDto> dtos = new ArrayList<RecommendWordDto>();
				for(KeywordRecommend kr:recommends){
					RecommendWordDto dto = new RecommendWordDto();
					dto.setWord(kr.getWord());
					dto.from(kr, device);
					dto.from(kr.getTags());
					dtos.add(dto);
				}
				result.setResult(dtos);
			}
			
		}
		
		return result;
	}
	
	@RequestMapping(method = RequestMethod.GET,path="/{adgroup_id}/combines")
	@ResponseBody
	public DeferredResult<List<RecommendWordDto>> combines(@PathVariable("adgroup_id") long adgroupID,
			@RequestParam(name="words")String[] words,
			@RequestParam(name="mode", defaultValue="0")int modeValue,
			@RequestParam(name="device", defaultValue="0")int deviceValue,
			@RequestParam(name="sort", defaultValue="pv")String sort,
			@RequestParam(name="direction", defaultValue="desc")String directionValue,
			@RequestParam(name="maxPrice", defaultValue="0")final float maxPrice,
			@RequestParam(name="defaultPrice", defaultValue="0")final float defaultPrice){
		final DeferredResult<List<RecommendWordDto>> result = new DeferredResult<List<RecommendWordDto>>();
		
		final Mode mode = Mode.from(modeValue);
		Direction direction = Direction.fromStringOrNull(directionValue);
		if(direction == null){
			direction = Direction.DESC;
		}
		final Device device = Device.from(deviceValue);
		sort = this.covertSort(sort, device);
		
		
		if(words != null && words.length > 0){
			Date yesterday = DateUtils.addDays(DateUtils.truncate(new Date(), Calendar.DATE), -1);
			Date start = DateUtils.addDays(yesterday, -params.getStateDays());
			
			List<AdgroupRecord> records = this.adgroupRecordRepository.findByAdgroupID(adgroupID, new Sort(new Order(Direction.DESC, "postTime")));
			final AdgroupRecord record = (records != null && records.size() > 0)?records.get(0):null;
			if(record == null){
				throw new IllegalArgumentException("record_not_found");
			}
			
			List<String> wordList = WordUtils.combine(words);
			
			final List<KeywordRecommend> recommends = this.keywordRecommendRepository.findByAdgroupIDAndWordIn(adgroupID, wordList);
			for(KeywordRecommend recommend: recommends){
				int index = wordList.indexOf(recommend.getWord());
				if(index > -1){
					wordList.remove(index);
				}
			}
			
			if(wordList.size() > 0){
				final List<KeywordRecommend> manualRecommends = new ArrayList<KeywordRecommend>();
				for(String word: wordList){
					KeywordRecommend recommend = new KeywordRecommend();
					recommend.setOrigin(Origin.manual);
					recommend.setAdgroupID(record.getAdgroupID());
					recommend.setAdgroupRecordID(record.getRecordID());
					recommend.setWord(word);
					recommend.setWeight(71);
					
					recommend.setCreateTime(new Date());
					recommend.setLastModifiedTime(new Date());
					manualRecommends.add(recommend);
				}
				this.keywordMetricService.syncInsightData4Recommends(manualRecommends, start, yesterday, record, new KeywordMetricService.Callback() {
					
					@Override
					public void call(List<KeywordRecommend> fetchedRecommends) {
						keywordRecommendRepository.save(fetchedRecommends);
						keywordRecommendService.syncRecommendTags(fetchedRecommends, record);
						recommends.addAll(fetchedRecommends);
						keywordRecommendPriceService.fetchPqr(mode.name(), recommends, record, device);
						keywordRecommendPriceService.fetchPrice(mode.name(), recommends, record, device, (int) (defaultPrice * 100), (int) (maxPrice * 100));
						
						List<RecommendWordDto> dtos = new ArrayList<RecommendWordDto>();
						for(KeywordRecommend kr:recommends){
							RecommendWordDto dto = new RecommendWordDto();
							dto.setWord(kr.getWord());
							dto.from(kr, device);
							dto.from(kr.getTags());
							dtos.add(dto);
						}
						result.setResult(dtos);
					}
				});
				
			}else{
				keywordRecommendPriceService.fetchPqr(mode.name(), recommends, record, device);
				keywordRecommendPriceService.fetchPrice(mode.name(), recommends, record, device, (int) (defaultPrice * 100), (int) (maxPrice * 100));
				List<RecommendWordDto> dtos = new ArrayList<RecommendWordDto>();
				for(KeywordRecommend kr:recommends){
					RecommendWordDto dto = new RecommendWordDto();
					dto.setWord(kr.getWord());
					dto.from(kr, device);
					dto.from(kr.getTags());
					dtos.add(dto);
				}
				result.setResult(dtos);
			}
			
		}
		
		return result;
	}
	
	@RequestMapping(method = RequestMethod.GET,path="/{adgroup_id}/relateds")
	@ResponseBody
	public DeferredResult<List<RecommendWordDto>> relateds(@PathVariable("adgroup_id") long adgroupID,
			@RequestParam(name="words")String[] words,
			@RequestParam(name="mode", defaultValue="0")int modeValue,
			@RequestParam(name="device", defaultValue="0")int deviceValue,
			@RequestParam(name="sort", defaultValue="pv")String sort,
			@RequestParam(name="direction", defaultValue="desc")String directionValue,
			@RequestParam(name="maxPrice", defaultValue="0")final float maxPrice,
			@RequestParam(name="defaultPrice", defaultValue="0")final float defaultPrice){
		final DeferredResult<List<RecommendWordDto>> result = new DeferredResult<List<RecommendWordDto>>();
		
		final Mode mode = Mode.from(modeValue);
		Direction direction = Direction.fromStringOrNull(directionValue);
		if(direction == null){
			direction = Direction.DESC;
		}
		final Device device = Device.from(deviceValue);
		sort = this.covertSort(sort, device);
		
		if(words != null && words.length > 0){
			Date yesterday = DateUtils.addDays(DateUtils.truncate(new Date(), Calendar.DATE), -1);
			Date start = DateUtils.addDays(yesterday, -params.getStateDays());
			
			List<AdgroupRecord> records = this.adgroupRecordRepository.findByAdgroupID(adgroupID, new Sort(new Order(Direction.DESC, "postTime")));
			final AdgroupRecord record = (records != null && records.size() > 0)?records.get(0):null;
			if(record == null){
				throw new IllegalArgumentException("record_not_found");
			}
			
			List<String> queryWords = new ArrayList<String>();
			for(String word: words){
				queryWords.add(word);
			}
			
			List<KeywordRelation> relations = this.keywordRelationRepository.findBySourceWordInAndOriginAndAndWeightGreaterThan(queryWords, Origin.taobao, 60);
			List<String> relatedWords = new ArrayList<String>();
			for(KeywordRelation relation: relations){
				relatedWords.add(relation.getTargetWord());
				queryWords.remove(relation.getSourceWord());
			}
			
			if(queryWords.size() > 0){
				log.debug("sync {} words", queryWords.size());
				this.keywordRecommendService.syncRelateds(queryWords, record);
				relations = this.keywordRelationRepository.findBySourceWordInAndOriginAndAndWeightGreaterThan(queryWords, Origin.taobao, 60);
				
				for(KeywordRelation relation: relations){
					relatedWords.add(relation.getTargetWord());
					queryWords.remove(relation.getSourceWord());
				}
			}
			
			if(queryWords.size() > 0){
				log.debug("still {} words has no related word", queryWords.size());
			}
			
			
			final List<KeywordRecommend> recommends = this.keywordRecommendRepository.findByAdgroupIDAndWordIn(adgroupID, relatedWords);
			for(KeywordRecommend recommend: recommends){
				int index = relatedWords.indexOf(recommend.getWord());
				if(index > -1){
					relatedWords.remove(index);
				}
			}
			
			if(relatedWords.size() > 0){
				List<KeywordRecommend> manualRecommends = new ArrayList<KeywordRecommend>();
				for(String word: relatedWords){
					KeywordRecommend recommend = new KeywordRecommend();
					recommend.setOrigin(Origin.relation);
					recommend.setAdgroupID(record.getAdgroupID());
					recommend.setAdgroupRecordID(record.getRecordID());
					recommend.setWord(word);
					recommend.setWeight(41);
					
					recommend.setCreateTime(new Date());
					recommend.setLastModifiedTime(new Date());
					manualRecommends.add(recommend);
				}
				this.keywordRecommendRepository.save(manualRecommends);
				this.keywordMetricService.syncInsightData4Recommends(manualRecommends, start, yesterday, record, new KeywordMetricService.Callback() {
					
					@Override
					public void call(List<KeywordRecommend> fetchedRecommends) {
						keywordRecommendService.syncRecommendTags(fetchedRecommends, record);
						recommends.addAll(fetchedRecommends);
						keywordRecommendPriceService.fetchPqr(mode.name(), recommends, record, device);
						keywordRecommendPriceService.fetchPrice(mode.name(), recommends, record, device, (int) (defaultPrice * 100), (int) (maxPrice * 100));
						List<RecommendWordDto> dtos = new ArrayList<RecommendWordDto>();
						for(KeywordRecommend kr:recommends){
							RecommendWordDto dto = new RecommendWordDto();
							dto.setWord(kr.getWord());
							dto.from(kr, device);
							dto.from(kr.getTags());
							dtos.add(dto);
						}
						result.setResult(dtos);
					}
				});
				
				
			}else{
				keywordRecommendPriceService.fetchPqr(mode.name(), recommends, record, device);
				keywordRecommendPriceService.fetchPrice(mode.name(), recommends, record, device, (int) (defaultPrice * 100), (int) (maxPrice * 100));
				List<RecommendWordDto> dtos = new ArrayList<RecommendWordDto>();
				for(KeywordRecommend kr:recommends){
					RecommendWordDto dto = new RecommendWordDto();
					dto.setWord(kr.getWord());
					dto.from(kr, device);
					dto.from(kr.getTags());
					dtos.add(dto);
				}
				result.setResult(dtos);
			}
			
			
			
		}
		
		return result;
	}
	
	private AdgroupRecord filterByStatus(List<AdgroupRecord> records, Status status){
		for(AdgroupRecord rec: records){
			if(rec.getStatus() == status){
				return rec;
			}
		}
		return null;
	}
	
	private String covertSort(String sort, Device device){
		if(device == Device.mobile){
			sort = MOBILE_MAPPINGS.get(sort);
			if(sort == null){
				sort = MOBILE_MAPPINGS.get("pv");
			}
		}else{
			sort = MAPPINGS.get(sort);
			if(sort == null){
				sort = MAPPINGS.get("pv");
			}
		}
		return sort;
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		MAPPINGS.put("pv", "insight.impression");
		MAPPINGS.put("click", "insight.click");
		MAPPINGS.put("ctr", "insight.ctr");
		MAPPINGS.put("avg_price", "insight.cpc");
		MAPPINGS.put("competition", "insight.competition");
		MAPPINGS.put("coverage", "insight.coverage");
		
		MOBILE_MAPPINGS.put("pv", "mobileInsight.impression");
		MOBILE_MAPPINGS.put("click", "mobileInsight.click");
		MOBILE_MAPPINGS.put("ctr", "mobileInsight.ctr");
		MOBILE_MAPPINGS.put("avg_price", "mobileInsight.cpc");
		MOBILE_MAPPINGS.put("competition", "mobileInsight.competition");
		MOBILE_MAPPINGS.put("coverage", "mobileInsight.coverage");
	}
}
